/*!
 * Parse request bodies.
 * https://github.com/cojs/co-body
 */
import zlib from 'node:zlib'
import qs from 'node:querystring'
import raw from './rawBody'
import { typeOfRequest } from './mime'

/**
 * Automatically unzip an HTTP stream.
 *
 * @param stream
 * @param options
 * @return {{headers}|*}
 */
function inflate(stream, options = {}) {
  if(!stream) {
    throw new TypeError('argument stream is required')
  }

  const encoding = options.encoding
    || (stream.headers && stream.headers['content-encoding'])
    || 'identity'

  switch(encoding) {
    case 'gzip':
    case 'deflate':
      break
    case 'identity':
      return stream
    default:
      const err = new Error('Unsupported Content-Encoding: ' + encoding)
      err.status = 415
      throw err
  }

  // no not pass-through encoding
  delete options.encoding

  return stream.pipe(zlib.Unzip(options))
}

/**
 * Return a Promise which parses text/plain requests.
 *
 * Pass a node request or an object with `.req`,
 * such as a koa Context.
 *
 * @param {Request} req
 * @param {Options} [opts]
 * @return {Function}
 */
export async function text(req, opts) {
  req = req.req || req
  opts = { ...opts }

  // defaults
  const len = req.headers['content-length']
  const encoding = req.headers['content-encoding'] || 'identity'
  if(len && encoding === 'identity') opts.length = ~~len
  opts.encoding = opts.encoding === undefined ? 'utf8' : opts.encoding
  opts.limit = opts.limit || '1mb'

  const str = await raw(inflate(req), opts)
  // ensure return the same format with json / form
  return opts.returnRawBody ? { parsed: str, raw: str } : str
}

// Allowed whitespace is defined in RFC 7159
// http://www.rfc-editor.org/rfc/rfc7159.txt
const _strictJSONReg = /^[\x20\x09\x0a\x0d]*([\[{])/

/**
 * Return a Promise which parses json requests.
 *
 * Pass a node request or an object with `.req`,
 * such as a koa Context.
 *
 * @param {Request} req
 * @param {Options} [opts]
 * @return {Function}
 */
export async function json(req, opts) {
  req = req.req || req
  opts = { ...opts }

  // defaults
  const len = req.headers['content-length']
  const encoding = req.headers['content-encoding'] || 'identity'
  if(len && encoding === 'identity') opts.length = ~~len
  opts.encoding = opts.encoding || 'utf8'
  opts.limit = opts.limit || '1mb'
  const strict = opts.strict !== false

  const str = await raw(inflate(req), opts)
  try {
    const parsed = parse(str)
    return opts.returnRawBody ? { parsed, raw: str } : parsed
  } catch(err) {
    err.status = 400
    err.body = str
    throw err
  }

  function parse(str) {
    if(!strict) return str ? JSON.parse(str) : str
    // strict mode always return object
    if(!str) return {}
    // strict JSON test
    if(!_strictJSONReg.test(str)) {
      throw new SyntaxError('invalid JSON, only supports object and array')
    }
    return JSON.parse(str)
  }
}

/**
 * Return a Promise which parses x-www-form-urlencoded requests.
 *
 * Pass a node request or an object with `.req`,
 * such as a koa Context.
 *
 * @param {Request} req
 * @param {Options} [opts]
 * @return {Function}
 */
export async function form(req, opts) {
  req = req.req || req
  opts = { ...opts }
  const queryString = opts.queryString || {}

  // keep compatibility with qs@4
  if(queryString.allowDots === undefined) queryString.allowDots = true

  // defaults
  const len = req.headers['content-length']
  const encoding = req.headers['content-encoding'] || 'identity'
  if(len && encoding === 'identity') opts.length = ~~len
  opts.encoding = opts.encoding || 'utf8'
  opts.limit = opts.limit || '56kb'
  opts.qs = opts.qs || qs

  const str = await raw(inflate(req), opts)
  try {
    const parsed = opts.qs.parse(str, queryString)
    return opts.returnRawBody ? { parsed, raw: str } : parsed
  } catch(err) {
    err.status = 400
    err.body = str
    throw err
  }
}

const jsonTypes = ['json', 'application/*+json', 'application/csp-report']
const formTypes = ['urlencoded']
const textTypes = ['text']

/**
 * Return a Promise which parses form and json requests
 * depending on the Content-Type.
 *
 * Pass a node request or an object with `.req`,
 * such as a koa Context.
 *
 * @param {Request} req
 * @param {Options} [opts]
 * @return {Function}
 * @api public
 */
export default async function any(req, opts) {
  req = req.req || req
  opts = opts || {}

  // json
  const jsonType = opts.jsonTypes || jsonTypes
  if(typeOfRequest(req, jsonType)) return json(req, opts)

  // form
  const formType = opts.formTypes || formTypes
  if(typeOfRequest(req, formType)) return form(req, opts)

  // text
  const textType = opts.textTypes || textTypes
  if(typeOfRequest(req, textType)) return text(req, opts)

  // invalid
  const type = req.headers['content-type'] || ''
  const message = type ? 'Unsupported content-type: ' + type : 'Missing content-type'
  const err = new Error(message)
  err.status = 415
  throw err
}
